// 
//   Copyright © 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
//  
//   This program is free software: you can redistribute it and/or modify
//   it under the terms of the GNU Affero General Public License as
//   published by the Free Software Foundation, either version 3 of the
//   License, or (at your option) any later version.
//  
//   This program is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU Affero General Public License for more details.
//  
//   You should have received a copy of the GNU Affero General Public License
//   along with this program.  If not, see <http://www.gnu.org/licenses/>.
//  
//  


using System;
using System.IO;
using System.Threading;
using System.Collections.Generic;
using System.Security.Cryptography;

using Anculus.Core;

using Galaxium.Protocol.Xmpp.Library.Xml;

namespace Galaxium.Protocol.Xmpp.Library.Streams
{
	public class FileTransferEventArgs: EventArgs
	{
		public FileTransfer Transfer { get; private set; }
		public FileTransferEventArgs (FileTransfer transfer) { Transfer = transfer; }
	}
	
	public class FileTransfer: StreamInitiator
	{
		private static int _sid_counter = 0;
	
		public event EventHandler Initiated;
		public event EventHandler Finished;
		public event EventHandler<TextEventArgs> Rejected;
		public event EventHandler<TextEventArgs> Failed;
		
		public FileTransfer (Client client, JabberID target, string path, string description)
			:base (client, target, Namespaces.SIFileTransfer, "FT" + (_sid_counter ++).ToString ())
		{
			if (!File.Exists (path))
				throw new ArgumentException ();
			
			Initiator = null;
			Target = target;
			FileName = Path.GetFileName (path);
			FilePath = path;
			var file = File.OpenRead (FilePath);
			FileSize = file.Length;
			file.Close ();
			Description = description;
		}
		
		public FileTransfer (JabberID initiator, string sid, string name, long size,
		                     string description, ManualResetEvent wait_handle)
			:base (null, null, Namespaces.SIFileTransfer, sid)
		{
			Initiator = initiator;
			Target = null;
			FileName = name;
			FileSize = size;
			Description = description;
			_wait_handle = wait_handle;
		}
		
		public JabberID EntityUID { get { return Target ?? Initiator; } }
		
		public JabberID Initiator { get; private set; }
		public JabberID Target { get; private set; }
		
		public string FileName { get; private set; }
		public string FilePath { get; set; }
		
		public DateTime FileMTime { get; private set; }
		public long FileSize { get; private set; }
		
		public string Description { get; private set; }
		
		public long Offset { get; private set; }
		public long RequestedLength { get; private set; }
		
		public long Transferred {
			get;
			private set;
		}
		
		internal bool IsAccepted { get; private set; }
		internal string DeclineReason { get; private set; }
		
		private Stream _stream;
		
		private ManualResetEvent _wait_handle;
		
		public void Offer ()
		{
			var name = Path.GetFileName (FilePath);
			var mtime = File.GetLastWriteTimeUtc (FilePath);
			var hash = (string) null;
			// TODO: checksum computation
			// TODO: MIME type detection
			Initiate (name, "application/octet-stream", FileSize, mtime, hash, Description);
		}
		
		public void Accept ()
		{
			if (_wait_handle == null)
				throw new InvalidOperationException ();
			
			IsAccepted = true;
			_wait_handle.Set ();
		}
		
		public void Decline (string reason)
		{
			if (_wait_handle == null)
				throw new InvalidOperationException ();
			
			DeclineReason = reason;
			IsAccepted = false;
			_wait_handle.Set ();
		}
		
		public void Abort ()
		{
			if (_stream == null)
				throw new InvalidOperationException ();
			
			_stream.Close ();
			_stream = null;
		}
		
		private void Initiate (string name, string mime_type, long size, DateTime mtime,
		                       string md5_hash, string description)
		{
			var file = new Element ("file", Namespaces.SIFileTransfer);
			file ["name"] = name;
			file ["size"] = size.ToString ();
			file ["time"] = mtime.ToUniversalTime ().ToString ("yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'");
			if (md5_hash != null)
				file ["hash"] = md5_hash;
			if (!String.IsNullOrEmpty (description))
				file.AppendChild (new Element ("desc").SetText (description));
			file.AppendChild (new Element ("range"));
			base.Initiate (mime_type, new Element [] { file });
		}
		
		protected override void OnInitiated (Stream stream, IList<Element> data)
		{
			Offset = 0;
			RequestedLength = Int64.MaxValue;
			
			_stream = stream;
			
			if (data.Count != 0) {
				var range = data [0].FirstChild ("range", null);
				if (range != null) {
					try { Offset = Int64.Parse (range ["offset"]); }
					catch {}
					try { RequestedLength = Int64.Parse (range ["length"]); }
					catch {}
				}
			}
			
			var thread = new Thread (SendData);
			thread.IsBackground = true;
			thread.Start ();
		}

		private void SendData ()
		{
			if (Initiated != null)
				Initiated (this, EventArgs.Empty);
			
			IsAccepted = true;
			
			var file = File.OpenRead (FilePath);
			file.Seek (Offset, SeekOrigin.Begin);
			
			Transferred = Offset;
			
			var block_size = _stream is IChunkedStream ?
				(_stream as IChunkedStream).ChunkSize : 4096;
			
			var buffer = new Byte [block_size];
			var offset = (long) 0;
			
			int count;
			do {
				count = (int) Math.Min (block_size, RequestedLength - offset);
				count = file.Read (buffer, 0, block_size);
				offset += count;
				try {
					_stream.Write (buffer, 0, count);
					Transferred += count;
				}
				catch (Exception e) {
					Log.Error (e, "Exception occured in while sending.");
					if (Failed != null)
						Failed (this, new TextEventArgs ("Exception occured in while sending."));
					return;
				}
			} while (count != 0);
			
			file.Close ();
			_stream.Close ();
			
			if (Finished != null)
				Finished (this, EventArgs.Empty);
		}
		
		internal void HandleStream (StreamRequestResult result, Stream stream, string path, bool append)
		{
			if (result != StreamRequestResult.Success) {
				
				string text = null;
				
				switch (result) {
					case StreamRequestResult.ProxyError:
						text = "Proxy negotiation failed.";
						break;
					case StreamRequestResult.Rejected:
						text = "Stream was rejected.";
						break;
					case StreamRequestResult.TimedOut:
						text = "Negotiation timed out.";
						break;
					case StreamRequestResult.UnknownFailure:
						text = "Unknown failure occured.";
						break;
					case StreamRequestResult.Unsupported:
						text = "Entity doesn't support the stream method it negotiated. WTF??";
						break;
				}
				
				if (Failed != null)
					Failed (this, new TextEventArgs (text));
			
				return;
			}
			
			_stream = stream;
			
			var thread = new Thread (() => {
				Log.Info ("Started receiving file at location " + path + ".");
				
				if (Initiated != null)
					Initiated (this, EventArgs.Empty);
				
				var file = File.Open (path, append ? FileMode.Append : FileMode.Create);
				
				var block_size = stream is IChunkedStream ? (stream as IChunkedStream).ChunkSize : 1024;
				
				var total = 0;
				
				var buffer = new byte [block_size];
				var count = 1;
				while (count != 0) {
					try {
						count = stream.Read (buffer, 0, block_size);
						Transferred += count;
					}
					catch { count = 0; }
					try {
						file.Write (buffer, 0, count);
					}
					catch (IOException e) {
						Log.Error (e, "Error writing file to disk.");
						if (Failed != null)
							Failed (this, new TextEventArgs (e.Message));
						this.Abort ();
						_stream = null;
						return;
					}
					total += count;
				}
			
				file.Close ();
			
				Log.Info ("Receiving finished. Received total of " + total + " bytes.");
				
				// remove the .path extension
				if (Transferred == FileSize) {
					var new_path = path.Substring (0, path.Length - 5);
					try { File.Delete (new_path); }
					catch {}
					try { File.Move (path, new_path); }
					catch {}
				
					if (Finished != null)
						Finished (this, EventArgs.Empty);
				}
				else {
					if (!append)
						File.Delete (path);
					if (Failed != null)
						Failed (this, new TextEventArgs ("Stream was closed."));
				}
				
				_stream = null;
			});
			thread.IsBackground = true;
			thread.Start ();
		}
		
		protected override void OnInitiateFailure (InitiateFailureReason reason, string text)
		{
			IsAccepted = false;
			
			string reason_text = null;
			
			switch (reason) {
				case InitiateFailureReason.BadProfile:
					reason_text = "Contact doesn't support file-transfer.";
					break;
				case InitiateFailureReason.NoValidStreams:
					reason_text = "Contact doesn't support any proposed transfer methods.";
					break;
				case InitiateFailureReason.Rejected:
					reason_text = "Transfer rejected.";
					if (!String.IsNullOrEmpty (text))
						reason_text += " (" + text + ")";
					break;
				case InitiateFailureReason.Timeout:
					reason_text = "Request timed out.";
					break;
				case InitiateFailureReason.Unknown:
					reason_text = "Unknown error.";
					break;
			}

			if (Rejected != null)
				Rejected (this, new TextEventArgs (reason_text));
		}
	}
}
